// Copyright 2016 Todd Fleming
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

import { basic, basic2d, square } from "./basic"
import { gcode } from "./GcodePreview"
import { image } from "./image"
import { imageMesh } from "./imageMesh"
import { thickLines } from "./thick-lines"

import { barrelDistort } from "./webcamfx"

export class DrawCommands {
  constructor(gl) {
    this.gl = gl
    this.EXT_blend_minmax = gl.getExtension("EXT_blend_minmax")
    this.WEBGL_lose_context = gl.getExtension("WEBGL_lose_context")
    this.glBuffer = this.gl.createBuffer()
    this.buffers = [this.glBuffer]
    this.textures = []
    this.frameBuffers = []
    this.shaders = []
    this.programs = []

    this.basic = basic(this)
    this.basic2d = basic2d(this)
    this.square = square(this)
    this.image = image(this)
    this.imageMesh = imageMesh(this)
    this.thickLines = thickLines(this)
    this.gcode = gcode(this)

    this.barrelDistort = barrelDistort(this)
  }

  destroy() {
    for (let buffer of this.buffers) this.gl.deleteBuffer(buffer)
    for (let texture of this.textures) this.gl.deleteTexture(texture)
    for (let program of this.programs) this.gl.deleteProgram(program)
    for (let frameBuffer of this.frameBuffers)
      this.gl.deleteFramebuffer(frameBuffer)
    for (let shader of this.shaders) this.gl.deleteShader(shader)
    if (this.WEBGL_lose_context) this.WEBGL_lose_context.loseContext()
  }

  createBuffer(data) {
    let buffer = this.gl.createBuffer()
    this.buffers.push(buffer)
    let result = {
      buffer,
      isBuffer: true,
      drawCommands: this,
      setData(data) {
        this.drawCommands.gl.bindBuffer(
          this.drawCommands.gl.ARRAY_BUFFER,
          buffer
        )
        this.drawCommands.gl.bufferData(
          this.drawCommands.gl.ARRAY_BUFFER,
          data,
          this.drawCommands.gl.STATIC_DRAW
        )
        this.drawCommands.gl.bindBuffer(this.drawCommands.gl.ARRAY_BUFFER, null)
      },
      destroy() {
        this.drawCommands.gl.deleteBuffer(this.buffer)
      },
    }
    result.setData(data)
    return result
  }

  createTexture(props) {
    let texture = this.gl.createTexture()
    this.textures.push(texture)
    let result = {
      texture,
      drawCommands: this,
      set({ image, width, height }) {
        let gl = this.drawCommands.gl
        gl.bindTexture(gl.TEXTURE_2D, texture)
        if (image) {
          this.width = image.width
          this.height = image.height
          gl.texImage2D(
            gl.TEXTURE_2D,
            0,
            gl.RGBA,
            gl.RGBA,
            gl.UNSIGNED_BYTE,
            image
          )
        } else if (this.width !== width || this.height !== height) {
          this.width = width
          this.height = height
          gl.texImage2D(
            gl.TEXTURE_2D,
            0,
            gl.RGBA,
            width,
            height,
            0,
            gl.RGBA,
            gl.UNSIGNED_BYTE,
            null
          )
        }
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
        gl.bindTexture(gl.TEXTURE_2D, null)
      },
      destroy() {
        this.drawCommands.gl.deleteTexture(this.texture)
      },
    }
    result.set(props)
    return result
  }

  createFrameBuffer(width, height) {
    let frameBuffer = this.gl.createFramebuffer()
    this.frameBuffers.push(frameBuffer)
    let texture = this.createTexture({ width, height })
    let result = {
      width,
      height,
      frameBuffer,
      texture,
      resize(width, height) {
        this.texture.set({ width, height })
      },
      destroy() {
        this.drawCommands.gl.deleteFramebuffer(this.frameBuffer)
        this.texture.destroy()
      },
    }
    this.useFrameBuffer(result, () => {
      this.gl.framebufferTexture2D(
        this.gl.FRAMEBUFFER,
        this.gl.COLOR_ATTACHMENT0,
        this.gl.TEXTURE_2D,
        texture.texture,
        0
      )
    })
    return result
  }

  useFrameBuffer(frameBuffer, next) {
    let old = this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING)
    this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, frameBuffer.frameBuffer)
    next()
    this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, old)
  }

  compile({ vert, frag, attrs }) {
    let comp = (type, source) => {
      let shader = this.gl.createShader(type)
      this.shaders.push(shader)
      this.gl.shaderSource(shader, source)
      this.gl.compileShader(shader)
      if (this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS))
        return shader
      else console.error(this.gl.getShaderInfoLog(shader))
    }
    let v = comp(this.gl.VERTEX_SHADER, vert)
    let f = comp(this.gl.FRAGMENT_SHADER, frag)
    if (!v || !f) return
    let program = this.gl.createProgram()
    this.programs.push(program)
    this.gl.attachShader(program, v)
    this.gl.attachShader(program, f)
    this.gl.linkProgram(program)
    if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
      console.error("shader link failed")
      return
    }
    let numUniforms = this.gl.getProgramParameter(
      program,
      this.gl.ACTIVE_UNIFORMS
    )
    let uniforms = []
    for (let i = 0; i < numUniforms; ++i) {
      let { name, size, type } = this.gl.getActiveUniform(program, i)
      uniforms.push({
        name,
        size,
        type,
        location: this.gl.getUniformLocation(program, name),
      })
    }
    let numAttrs = this.gl.getProgramParameter(
      program,
      this.gl.ACTIVE_ATTRIBUTES
    )
    let result = { program, uniforms, attrs: [] }
    for (let i = 0; i < numAttrs; ++i) {
      let { name, size, type } = this.gl.getActiveAttrib(program, i)
      if (type == this.gl.FLOAT_VEC2) {
        type = this.gl.FLOAT
        size = 2
      } else if (type == this.gl.FLOAT_VEC3) {
        type = this.gl.FLOAT
        size = 3
      } else if (type == this.gl.FLOAT_VEC4) {
        type = this.gl.FLOAT
        size = 4
      }
      let attr = attrs[name]
      if (!attr) {
        console.error("missing attr", name)
        return
      }
      result.attrs.push({ ...attr, name, size, type })
    }
    this.generateUseUniforms(result)
    this.generateUseAttrs(result)
    return result
  }

  generateUseUniforms(program) {
    let { uniforms } = program
    let body = "let {" + uniforms.map((u) => u.name) + "} = props;\n"
    body += "let gl = drawCommands.gl;\n"
    let numTextures = 0
    for (let uniform of uniforms) {
      program[uniform.name + "_location"] = uniform.location
      let set
      switch (uniform.type) {
        case this.gl.BOOL:
          set =
            "gl.uniform1i(this." +
            uniform.name +
            "_location, " +
            uniform.name +
            ");\n"
          break
        case this.gl.FLOAT:
          set =
            "gl.uniform1f(this." +
            uniform.name +
            "_location, " +
            uniform.name +
            ");\n"
          break
        case this.gl.FLOAT_VEC2:
          set =
            "gl.uniform2fv(this." +
            uniform.name +
            "_location, " +
            uniform.name +
            ");\n"
          break
        case this.gl.FLOAT_VEC3:
          set =
            "gl.uniform3fv(this." +
            uniform.name +
            "_location, " +
            uniform.name +
            ");\n"
          break
        case this.gl.FLOAT_VEC4:
          set =
            "gl.uniform4fv(this." +
            uniform.name +
            "_location, " +
            uniform.name +
            ");\n"
          break
        case this.gl.FLOAT_MAT2:
          set =
            "gl.uniformMatrix2fv(this." +
            uniform.name +
            "_location, false, " +
            uniform.name +
            ");\n"
          break
        case this.gl.FLOAT_MAT3:
          set =
            "gl.uniformMatrix3fv(this." +
            uniform.name +
            "_location, false, " +
            uniform.name +
            ");\n"
          break
        case this.gl.FLOAT_MAT4:
          set =
            "gl.uniformMatrix4fv(this." +
            uniform.name +
            "_location, false, " +
            uniform.name +
            ");\n"
          break
        case this.gl.SAMPLER_2D:
          body +=
            "gl.activeTexture(" + (this.gl.TEXTURE0 + numTextures) + ");\n"
          body +=
            "gl.bindTexture(gl.TEXTURE_2D, " + uniform.name + ".texture);\n"
          body +=
            "gl.uniform1i(this." +
            uniform.name +
            "_location, " +
            numTextures +
            ");\n"
          ++numTextures
          continue
        default:
          console.error(
            "uniform",
            uniform.name,
            "type",
            uniform.type.toString(16),
            "size",
            uniform.size,
            "unhandled"
          )
          continue
      }
      // body += 'console.log(this);\n';
      // body += 'console.log(this.uniforms, "' + uniform.name + '");\n';
      // body += 'console.log(this.' + uniform.name + '_location);\n';
      body += "if (" + uniform.name + " !== this." + uniform.name + ") {\n"
      body += "    " + set
      body += "    this." + uniform.name + " = " + uniform.name + ";\n"
      body += "}\n"
    }
    body += "next();\n"
    for (let i = 0; i < numTextures; ++i) {
      body += "drawCommands.gl.activeTexture(" + (this.gl.TEXTURE0 + i) + ");\n"
      body += "drawCommands.gl.bindTexture(gl.TEXTURE_2D, null);\n"
    }
    program.useUniforms = new Function("drawCommands", "props", "next", body)
  }

  generateUseAttrs(program) {
    let { attrs } = program
    let setup = ""
    let teardown = ""
    for (let i = 0; i < attrs.length; ++i) {
      let attr = attrs[i]
      setup +=
        "drawCommands.gl.vertexAttribPointer(" +
        i +
        ", " +
        attr.size +
        ", " +
        attr.type +
        ", false, stride, offset + " +
        attr.offset +
        ");\n"
      setup += "drawCommands.gl.enableVertexAttribArray(" + i + ");\n"
      teardown += "drawCommands.gl.disableVertexAttribArray(" + i + ");\n"
    }
    let body = setup + "next();\n" + teardown
    //console.log(body);
    program.useAttrs = new Function(
      "drawCommands",
      "stride",
      "offset",
      "next",
      body
    )
  }

  execute({ program, primitive, uniforms, buffer, attributes }) {
    let useBuffer = (next) => {
      if (buffer.data.isBuffer) {
        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer.data.buffer)
        next()
        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null)
      } else {
        if (!buffer.data.length) return
        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.glBuffer)
        this.gl.bufferData(
          this.gl.ARRAY_BUFFER,
          buffer.data,
          this.gl.DYNAMIC_DRAW
        )
        next()
        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null)
      }
    }

    let useProgram = (next) => {
      this.gl.useProgram(program.program)
      next()
      this.gl.useProgram(null)
    }

    useBuffer(() => {
      useProgram(() => {
        program.useUniforms(this, uniforms, () => {
          program.useAttrs(this, buffer.stride, buffer.offset, () => {
            this.gl.drawArrays(primitive, 0, buffer.count)
          })
        })
      })
    })
  } // execute
} // DrawCommands

export default DrawCommands
